youtube: gRPC Crash Course
https://www.youtube.com/watch?v=Yw4rkaTc0f8
google が 2015 年に開発した
http2.0 と protocol buffers を使う
Ageneda
Client / Server Communication
Client library の問題
なぜ gRPC が開発されたか
理由のない技術は許されないが、
gRPC が開発された理由は割といいらしい
gRPC
使い方が色々ある
Modes
Unary
単発
HTTP Request と同じ
Streaming
Server Streaming
大量のデータが有って、部分的にデシリアライズしつつ使いたい場合
Youtube のような動画サービスで便利
Client Streaming
巨大ファイルをアップロードする
チャンクごとに画像やビデオを上げて、部分づつエンコーディングするなど
Bidirectional
チャット、ゲームサーバーなど
リアルタイム性のある通信に
Clinet / Server サイドで Unary/Streaming 両方ができる
Bidirectional
Protocols
SOAP, REST, GraphQL
SSE, WebSockets
ServerSide Eventing ?
Raw TCP
Problem with Client Library
どのプロトコルも、クライアントライブラリーが必要 (言語に沿った)
HTTP library, SOAP library
双方向通信の仕組みが WebSocket が出るまでなかった
ただし、大量のバイトを送るだけでスキーマなどがない
client library が複数あるが、それぞれを維持するのが難しい
http/1.0, http/2 , new features, security
Why gRPC was invented?
Client Library
人気のある言語に1つだけライブラリー
Protocol: HTTP/2 を使う
実装はライブラリに隠されている
フレームワークを使うだけで魔法のような便利さを体感できる
Message format: ProtocolBuffers
言語に依存しない中間スキーマ言語
バイナリにシリアライズして通信、送信先でデシリアライズ
Coding!
Protobuf
code: .protobuf
syntax = "proto3";
// module system 的な
package todoPackage;
// Service: server に call できる rpc を定義する
service Todo {
rpc createTodo(TodoItem) returns (TodoItem);
rpc readTodos(unit) returns (TodoItems);
}
// rpc は、絶対に引数と戻り値がないといけないので、 unit 型を定義する
// void という名前を使うとバグる
message unit {}
message TodoItem{
int32 id = 1;
string text = 2;
}
message TodoItems {
repeated TodoItem items = 1;
}
Server
code: .ts
// noImplicitAny: false
import * as grpc from "grpc";
import * as protoLoader from "@grpc/proto-loader";
// loadsync() : 第二引数は、大文字をどうするかなどの読み込み設定を入れる
const packageDef = protoLoader.loadSync("todo.proto", {});
const grpcObject = grpc.loadPackageDefinition(packageDef);
const todoPackage = grpcObject.todoPackage;
const server = new grpc.Server();
// とりあえずお試しなので、第二引数は createInsecure()
// セキュリティ意識して作るときは createSSL()
server.bind("0.0.0.0:40000", grpc.ServerCredentials.createInsecure());
const todos: Array<any> = [];
server.addService(todoPackage.Todo.service, {
createTodo: (call, callback) => {
const item = {
id: todos.length + 1,
text: call.request.text,
};
console.log(item);
todos.push(item);
callback(null, item);
},
readTodos: (_call, callback) => {
callback(null, { items: todos });
},
});
server.start();
Nodejs の実装例
1. protoLoaderを使って、.protoを読み込んだ packageDef を返す
2. grpc モジュールで、packageDef から grpcObject を読み込む
3. todoPackage.Todo (Service type) の service プロパティに、それぞれハンドラーを割り当てる
多分、rpc のスキーマと一致する必要あり
AddService
code: .js
server.addService(todoPackage.Todo.service, {
createTodo: (call, callback) => {
console.log(call, callback);
console.log(call.request);
todos.push(call.request);
callback(null, {
id: todos.length,
text: call.request.text,
});
},
rpc のサービスを定義するところ
json の 辞書形式で、 { [service_name] : (ServerUnaryCall, [function sendUnaryData]) => voide }
を渡す
call: 相手先からの呼び出しのデータ
call.request : json
callback(bytesWrittenNumber, json): 相手に返す grpc レスポンス。jsonを入れると自動で ProtocolBuffers に変換してくれる
1番目の引数はとりあえず null にしとけば動く? (Stream 系の何かかも)
readTodos
code: .ts
{
readTodos: (_call, callback) => {
callback(null, { items: todos });
},
}
Array 系の Protobuf 定義は、一回 ラップして ~~s系の複数系 message にする。
なので、itemsフィールドを持つ json にして入れる
Client
code: .ts
import * as grpc from "grpc";
import * as protoLoader from "@grpc/proto-loader";
const packageDef = protoLoader.loadSync("todo.proto", {});
const grpcObject = grpc.loadPackageDefinition(packageDef);
const todoPackage = grpcObject.todoPackage as any;
const client = new todoPackage.Todo(
"localhost:40000",
grpc.credentials.createInsecure()
);
client.createTodo(
{
id: -1,
text: "Do Laundry",
},
(err, response) => console.log(JSON.stringify(response))
);
clientを作成し、そのメソッドを呼ぶだけ。
戻り値が callback で受け取る仕様なのが気になる (Promise にしろよと)
また、ここでも rpc client のメソッドに渡すのはただの json でいい
自動で ProtocolBuffers にしてくれる
code: .ts
const input = process.argv2; client.createTodo(
{
id: -1,
text: input ? input : "",
},
(err, response) => console.log(JSON.stringify(response))
);
ちょっとアレンジ
チャットっぽく、引数で投稿メッセージを変える
(何も入れないと undefined になる。向こう側が any で扱ってなかったらバグる)
Stream RPC
code: .protobuf
service Todo {
rpc createTodo(TodoItem) returns (TodoItem);
rpc readTodos(unit) returns (TodoItems);
rpc readTodosStream(unit) returns (stream TodoItem);
}
何らかの message type を Stream にするには、
stream という修飾子をつける。
これは、戻り値だけ stream になってるが、多分引数も stream にできそう
SeverSide Stream
code: .ts
const readTodosStream = (call, callback) => {
todos.forEach((t) => call.write(t));
call.end();
};
todo を一気に callback にわたすのではなく、
call.write(t)で一つづつ書き込んでいく。
終わったら .endメソッドを呼ぶ
Recieve Stream
code: .ts
const call: grpc.Call = client.readTodosStream();
call.on("data", (item) => {
console.log("recieved item from server " + JSON.stringify(item));
});
call.on("end", (e) => console.log("server done!"));
Node なので。
on()メソッドで、コールバックを設定しておく。
gRPC Pros & Cons
Pros
Fast & Compact
json の 1/3 以下
更に HTTP/2 で圧縮される
One Client Library
どの HTTP クライアントライブラリを使うか悩まない
クライアントライブラリを維持する人が消える心配がない (gRPC 公式のやつについては)
常に最新の HTTP 仕様が使える
Progress Feedback
どれくらいアップロードしたかなど、Streaming でチャンクごとに表示できればプログレスバー表示でフロントから出力できる
Cancel Request (H2)
リクエストのキャンセルというのは、Stateful な通信でないとできない
gRPC はステートフル、なのでキャンセルできる
すべての gRPC Stream Request/Response はIDタグ付けされる
call Object
キャンセルのためのコードを書く必要はある (再試行 or 諦める、後処理など)
H2/Protobuf
Cons
Forces Schema
class や struct の実装を矯正されるようなもの
簡単なものを作るときはむしろ邪魔かも
ただ、JSON Schema とかを書くくらいならむしろ Protobuf がその役目を果たすのでむしろいいかも
Thick Client
毎回 Protobuf をコンパイルしないとなので、レイヤーが厚い
Proxies
gRPC サーバーまでなんかしらの RESTful API の層が挟まってると、うまく届けてくれないかも
api gatewayとか
nginx は対応してるらしい
Still young
サポートが行き届いてないかも
Error handling
つらい
HTTP Status Code がないから
No native browser support
インターネットにはスキーマがない
Timeouts (pub/sub)
unary call の場合は、すべてのレスポンスが来るまで待つ必要がある
Why Spotify use gRPC?
Sportify は Hermes を使ってた
スキーマがクソだから移行したらしい